<template>
  <div class="scaner" ref="scanerRef">
    <i class="close" @click="closeScaner()"> </i>
    <div class="banner" v-if="showBanner">
      <svg-icon
        icon-class="closefull"
        class="cursor-pointer close_icon"
        @click="() => (showBanner = false)"
      />
      <p class="text">若当前浏览器无法扫码，请切换其他浏览器尝试</p>
    </div>
    <div class="cover">
      <p class="line"></p>
      <span class="square top left"></span>
      <span class="square top right"></span>
      <span class="square bottom right"></span>
      <span class="square bottom left"></span>
      <p class="tips">请将二维码放入框内</p>
    </div>
    <video
      v-show="showPlay"
      class="source"
      ref="videoRef"
      :width="videoWH.width"
      :height="videoWH.height"
      controls
    ></video>
    <canvas v-show="!showPlay" ref="canvasRef"></canvas>
    <button v-show="showPlay" @click="run">开始</button>
  </div>
</template>

<script setup lang="ts">
import jsQR from "jsqr";
import { computed } from "vue";
defineOptions({
  name: "Scaner",
  inheritAttrs: false,
});

const emit = defineEmits(["close-scaner", "code-scanned", "error-captured"]);

const props = defineProps({
  // 使用后置相机
  useBackCamera: {
    type: Boolean,
    default: true,
  },
  // 扫描识别后停止
  stopOnScaned: {
    type: Boolean,
    default: true,
  },
  drawOnfound: {
    type: Boolean,
    default: true,
  },
  // 线条颜色
  lineColor: {
    type: String,
    default: "#03C03C",
  },
  // 线条宽度
  lineWidth: {
    type: Number,
    default: 2,
  },
  // 视频宽度
  videoWidth: {
    type: Number,
    default: document.documentElement.clientWidth || document.body.clientWidth,
  },
  // 视频高度
  videoHeight: {
    type: Number,
    default:
      document.documentElement.clientHeight - 48 ||
      document.body.clientHeight - 48,
  },
  responsive: {
    type: Boolean,
    default: false,
  },
});

const {
  useBackCamera,
  stopOnScaned,
  drawOnfound,
  lineColor,
  lineWidth,
  videoWidth,
  videoHeight,
  responsive,
} = toRefs(props);

const showPlay = ref(false);
const showBanner = ref(true);
const containerWidth = ref(null);
const active = ref(false);
const canvas = ref();
const videoRef = ref();
const canvasRef = ref();
const scanerRef = ref();

const videoWH = computed(() => {
  if (containerWidth.value) {
    const width = containerWidth.value;
    const height = width * 0.75;
    return { width, height };
  }
  return { width: videoWidth.value, height: videoHeight.value };
});

watch(
  active,
  (newValue) => {
    if (!newValue) {
      fullStop();
    }
  },
  {
    immediate: true,
  }
);

onMounted(() => {
  setup();
});

onBeforeUnmount(() => {
  fullStop();
});

// 关闭摄像机
function closeScaner() {
  emit("close-scaner", false);
}

// 画线
interface DrawLineProp {
  x: string;
  y: string;
}
function drawLine(begin: DrawLineProp, end: DrawLineProp) {
  canvas.value.beginPath();
  canvas.value.moveTo(begin.x, begin.y);
  canvas.value.lineTo(end.x, end.y);
  canvas.value.lineWidth = lineWidth.value;
  canvas.value.strokeStyle = lineColor.value;
  canvas.value.stroke();
}
interface LocationProp<T> {
  topLeftCorner: T;
  topRightCorner: T;
  bottomRightCorner: T;
  bottomLeftCorner: T;
}
// 画框
function drawBox(location: LocationProp<DrawLineProp>) {
  if (drawOnfound.value) {
    drawLine(location.topLeftCorner, location.topRightCorner);
    drawLine(location.topRightCorner, location.bottomRightCorner);
    drawLine(location.bottomRightCorner, location.bottomLeftCorner);
    drawLine(location.bottomLeftCorner, location.topLeftCorner);
  }
}
interface codeProp<T, B> {
  location: T;
  data: B;
}
function tick() {
  if (
    videoRef.value &&
    videoRef.value.readyState === videoRef.value.HAVE_ENOUGH_DATA
  ) {
    canvasRef.value.height = videoWH.value.height;
    canvasRef.value.width = videoWH.value.width;
    canvas.value.drawImage(
      videoRef.value,
      0,
      0,
      canvasRef.value.width,
      canvasRef.value.height
    );
    const imageData = canvas.value.getImageData(
      0,
      0,
      canvasRef.value.width,
      canvasRef.value.height
    );
    let code: codeProp<LocationProp<DrawLineProp>, string> | boolean | any =
      false;
    try {
      code = jsQR(imageData.data, imageData.width, imageData.height);
    } catch (e) {
      console.error(e);
    }
    if (code) {
      drawBox(code.location);
      found(code.data);
    }
  }
  run();
}

// 初始化
const previousCode = ref();
const parity = ref();
function setup() {
  if (responsive.value) {
    nextTick(() => {
      containerWidth.value = scanerRef.value.clientWidth;
    });
  }
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    previousCode.value = null;
    parity.value = 0;
    active.value = true;
    canvas.value = canvasRef.value.getContext("2d");
    const facingMode = useBackCamera.value ? { exact: "environment" } : "user";
    const handleSuccess = (stream: any) => {
      if (videoRef.value.srcObject !== undefined) {
        videoRef.value.srcObject = stream;
      } else if (window.videoEl.mozSrcObject !== undefined) {
        videoRef.value.mozSrcObject = stream;
      } else if (window.URL.createObjectURL) {
        videoRef.value.src = window.URL.createObjectURL(stream);
      } else if (window.webkitURL) {
        videoRef.value.src = window.webkitURL.createObjectURL(stream);
      } else {
        videoRef.value.src = stream;
      }
      videoRef.value.playsInline = true;
      const playPromise = videoRef.value.play();
      playPromise.catch(() => (showPlay.value = true));
      playPromise.then(run());
    };
    navigator.mediaDevices
      .getUserMedia({ video: { facingMode } })
      .then(handleSuccess)
      .catch(() => {
        navigator.mediaDevices
          .getUserMedia({ video: true })
          .then(handleSuccess)
          .catch((error) => {
            emit("error-captured", error);
          });
      });
  }
}
function run() {
  if (active.value) {
    requestAnimationFrame(tick);
  }
}
function found(code: any) {
  if (previousCode.value !== code) {
    previousCode.value = code;
  } else if (previousCode.value === code) {
    parity.value += 1;
  }
  if (parity.value > 2) {
    active.value = stopOnScaned.value ? false : true;
    parity.value = 0;
    emit("code-scanned", code);
  }
}
// 完全停止
function fullStop() {
  if (videoRef.value && videoRef.value.srcObject) {
    videoRef.value.srcObject.getTracks().forEach((t: any) => t.stop());
  }
}
</script>

<style lang="scss" scoped>
.close {
  display: inline-block;
  height: 80px;
  width: 80px;
  background: url("@/assets/images/close.png") no-repeat center;
  background-size: auto 100%;
  position: absolute;
  color: #ffffff;
  left: 2px;
  top: 5px;
  z-index: 200;
}
.scaner {
  background: #000000;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  height: -webkit-calc(100%);
  height: -moz-calc(100%);
  height: -ms-calc(100%);
  height: -o-calc(100%);
  height: calc(100%);
}
.scaner .banner {
  width: 340px;
  position: absolute;
  top: 16px;
  left: 50%;
  margin-left: -170px;
  background: #e6a23c;
  border-radius: 8px;
  box-sizing: border-box;
  padding: 12px;
  opacity: 0.9;
  box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2);
  color: #ffffff;
}
.scaner .banner .text {
  padding: 0;
  margin: 0;
  color: #ffffff;
  font-size: 12px;
  text-align: justify;
  text-align-last: left;
}
.scaner .banner .close_icon {
  display: inline-block;
  height: 20px !important;
  width: 20px !important;
  position: absolute;
  right: 10px;
  top: 10px;
}
.scaner .cover {
  height: 300px;
  width: 320px;
  position: absolute;
  top: 40%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
  -moz-transform: translate(-50%, -50%);
  -ms-transform: translate(-50%, -50%);
  -o-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
  /* border: 0.5px solid #999999; */
  z-index: 1111;
}
.scaner .cover .line {
  width: 300px;
  height: 1px;
  margin-left: 10px;
  background: #5f68e8;
  background: linear-gradient(
    to right,
    transparent,
    #5f68e8,
    #0165ff,
    #5f68e8,
    transparent
  );
  position: absolute;
  -webkit-animation: scan 1.75s infinite linear;
  -moz-animation: scan 1.75s infinite linear;
  -ms-animation: scan 1.75s infinite linear;
  -o-animation: scan 1.75s infinite linear;
  animation: scan 1.75s infinite linear;
  -webkit-animation-fill-mode: both;
  -moz-animation-fill-mode: both;
  -ms-animation-fill-mode: both;
  -o-animation-fill-mode: both;
  animation-fill-mode: both;
  border-radius: 1px;
}
.scaner .cover .square {
  display: inline-block;
  height: 20px;
  width: 20px;
  position: absolute;
}
.scaner .cover .square.top {
  top: 0;
  border-top: 1px solid #5f68e8;
}
.scaner .cover .square.left {
  left: 0;
  border-left: 1px solid #5f68e8;
}
.scaner .cover .square.bottom {
  bottom: 0;
  border-bottom: 1px solid #5f68e8;
}
.scaner .cover .square.right {
  right: 0;
  border-right: 1px solid #5f68e8;
}
.scaner .cover .tips {
  position: absolute;
  bottom: -70px;
  width: 100%;
  text-align: center;
  font-size: 13px;
  color: #ffffff;
  opacity: 0.8;
}
@-webkit-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 50px;
  }
  50% {
    top: 100px;
  }
  75% {
    top: 150px;
  }
  100% {
    top: 200px;
  }
}
@-moz-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 50px;
  }
  50% {
    top: 100px;
  }
  75% {
    top: 150px;
  }
  100% {
    top: 200px;
  }
}
@-o-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 50px;
  }
  50% {
    top: 100px;
  }
  75% {
    top: 150px;
  }
  100% {
    top: 200px;
  }
}
@keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 50px;
  }
  50% {
    top: 100px;
  }
  75% {
    top: 150px;
  }
  100% {
    top: 200px;
  }
}
</style>
